import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';

import 'package:cloudapp/esdk/response/album_item_info.dart';
import 'package:cloudapp/esdk/response/file_list_item.dart';
import 'package:cloudapp/esdk/response/server_info_response.dart';
import 'package:cloudapp/exceptions/exceptions.dart';
import 'package:cloudapp/utils/utils.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';

import 'request/user_login_request.dart';
import 'response/error_response.dart';
import 'response/user_login_response.dart';

class ESync {
  final _dio = Dio();

  var _userId = 0;
  var _token = "";
  var _rnd = Random();
  var _userAgent = "";
  var _cancel = CancelToken();
  var _base = "";

  static ESync _esync;
  ESync() {
    _dio.options.connectTimeout = 5000;
    _dio.options.receiveTimeout = 7000;
  }

  factory ESync.i() {
    if (_esync == null) {
      _esync = ESync();
    }

    return _esync;
  }

  void setUserAgent(String ua) {
    _userAgent = ua;
    _userAgent += " uni-app";
  }

  void setToken(int userId, String token) {
    _userId = userId;
    _token = token;
  }

  void setServerHost(String host) {
    var ip = InternetAddress(host);
    _base = "http://${ip.address}:18083";
    print('set base host address $_base');
  }

  void setBase(String base) {
    _base = base;
    print('set base address $_base');
  }

  String getServerHost() {
    return _base;
  }

  String _getReqID() {
    var v = _rnd.nextDouble();
    var buffer = Uint8List(8).buffer;
    var bdata = ByteData.view(buffer);
    bdata.setFloat64(0, v);
    return bdata.getUint32(0).toRadixString(36);
  }

  get userId => _userId;
  get accessToken => _token;

  Future<String> _apiRequest(String method, String bizUrl,
      {String query, body}) async {
    method = method.toUpperCase();
    String contentType = "application/json";
    print("base url: " + _base);
    String fullUrl = _base + bizUrl;
    if (query != null && query.isNotEmpty) {
      fullUrl += "?" + query;
    }

    String authHeader = "USER $_userId $_token";
    var userAgent = "Flutter/ESync v0.1 Dio/3.1";
    if (_userAgent.isNotEmpty) {
      userAgent = _userAgent;
    }
    var headers = {
      "sdk-version": "0.1",
      "Authorization": authHeader,
      "User-Agent": userAgent,
      "ReqID": _getReqID(),
    };

    print('$method: [$fullUrl]');
    print('BODY: [$body]');
    _cancel = CancelToken();
    try {
      var resp = await _dio.request(
        fullUrl,
        data: body,
        cancelToken: _cancel,
        onSendProgress: (count, total) {
          var percent = ((count / total) * 100).toInt();
          if(percent % 10 == 0) {
            print('completed $percent%');
          }
        },
        options: Options(
          method: method,
          headers: headers,
          contentType: contentType,
          responseType: ResponseType.plain,
        ),
      );
      var result = resp.data.toString();
      print('RECV: [$result]');
      return result;
    } on DioError catch (e) {
      print('$bizUrl catched exception......');
//      print('exception: ${e.toString()}');
//      print(stack);
      if(e.type == DioErrorType.CONNECT_TIMEOUT ||
          e.type == DioErrorType.RECEIVE_TIMEOUT ||
          e.type == DioErrorType.SEND_TIMEOUT) {
        throw RequestException.onlyMessage('请求服务器超时,请重试');
      }
      if(e.type == DioErrorType.CANCEL) {
        throw CancelUploadException();
      }

      var resp = e.response;
      if(resp == null) {
        throw RequestException.onlyMessage('请求服务器失败');
      }
      if(resp.data == null) {
        throw RequestException.onlyMessage('服务器未正确响应');
      }

      var source = e.response.data.toString();
      var err = ErrorResponse.fromJson(unmarshalJson(source));
      throw RequestException(
        httpCode: e.response.statusCode,
        code: err.code,
        msg: err.msg,
      );
    } catch (e, stack) {
      print('unknown exception...... $e}');
      print(stack);
      throw RequestException(msg: "未知异常!");
    }
  }

  T unmarshalJson<T>(String body) {
    try {
      T t = jsonDecode(body);
      return t;
    } on FormatException catch (e) {
      print('json decode failed. ${e.toString()}');
      throw JsonDecodeError();
    }
  }

  Future<int> fullPictureUpload(int albumId, String fileName, String hash, String path) async {
    var bizUrl = "/api/v1/dcim/albums/$albumId/upload/full/picture";
    var method = "PUT";
    var formData = FormData.fromMap({
      "file": await MultipartFile.fromFile(path),
      "file_name": fileName,
      "hash": hash,
    });

    String source = await _apiRequest(method, bizUrl, body: formData);
    Map<String, dynamic> rsp = unmarshalJson(source);
    return rsp['file_id'];
  }

  Future<int> fullVideoUpload(int albumId, String fileName, String hash, String path, String thumbData) async {
    var bizUrl = "/api/v1/dcim/albums/$albumId/upload/full/video";
    var method = "PUT";
    var formData = FormData.fromMap({
      "file": await MultipartFile.fromFile(path),
      "file_name": fileName,
      "thumb": thumbData,
      "hash": hash,
    });

    String source = await _apiRequest(method, bizUrl, body: formData);
    Map<String, dynamic> rsp = unmarshalJson(source);
    return rsp['file_id'];
  }

  Future<int> quickUpload(int albumId, String fileName, String hash) async {
    var bizUrl = "/api/v1/dcim/albums/$albumId/upload/quick";
    var method = "PUT";
    var form = {
      "file_name": fileName,
      "hash": hash,
    };

    var source = await _apiRequest(method, bizUrl, body: jsonEncode(form));
    Map<String, dynamic> rsp = unmarshalJson(source);
    return rsp['file_id'];
  }

  Future<UserLoginResponse> userLogin(String user, String pwd) async {
    var method = "POST";
    var bizUrl = "/api/v1/user/login";
    var r = UserLoginRequest();
    r.userName = user;
    r.password = pwd;
    r.remember = 0;

    String body = jsonEncode(r);
    String rsp = await _apiRequest(method, bizUrl, body: body);
    return UserLoginResponse.fromJson(unmarshalJson(rsp));
  }

  Future<void> userLogout() async {
    var method = "POST";
    var bizUrl = "/api/v1/user/logout";
    await _apiRequest(method, bizUrl);
  }

  List<T> clsArrayConvert<T>(List<dynamic> r, factoryConstructor<T> f) {
    var out = List<T>();
    for(var item in r) {
      out.add(f(item));
    }
    return out;
  }

  Future<ServerInfoResponse> getServerInfo() async {
    var method = "GET";
    var bizUrl = "/api/v1/server/info";

    var rsp = await _apiRequest(method, bizUrl);
    return ServerInfoResponse.fromJson(unmarshalJson(rsp));
  }

  Future<List<AlbumItemInfo>> getAlbumList() async {
    var method = "GET";
    var bizUrl = "/api/v1/dcim/albums/";

    var rsp = await _apiRequest(method, bizUrl);
    List<dynamic> rs = unmarshalJson(rsp);

    return clsArrayConvert(rs, AlbumItemInfo.fromJson);
  }

  Future<List<FileListItem>> getAlbumFileList(int albumId) async {
    var method = "GET";
    var bizUrl = "/api/v1/dcim/albums/$albumId/files";

    var rsp = await _apiRequest(method, bizUrl);
    List<dynamic> rs = unmarshalJson(rsp);

    return clsArrayConvert(rs, FileListItem.fromJson);
  }

  Future<List<FileListItem>> getAlbumFileListAndResizeThumb(int albumId) async {
    var result = await getAlbumFileList(albumId);
    return await compute(scaleThumbs, result);
  }

  String getFileRawUrl(int fileID) {
    return "$_base/api/v1/dcim/files/$fileID/raw?user_id=$_userId&access_token=$_token";
  }

  Future<int> fetchAlbumId(int devId, String albumName) async {
    var method = "PUT";
    var bizUrl = "/api/v1/dcim/albums/";
    var req = {
      "device_id": devId,
      "name": albumName,
    };

    var rsp = await _apiRequest(method, bizUrl, body: jsonEncode(req));
    var devInfo = unmarshalJson<Map<String, dynamic>>(rsp);
    return devInfo['album_id'];
  }

  Future<int> fetchDeviceId(String brand, String model, String hash) async {
    var method = "PUT";
    var bizUrl = "/api/v1/server/device";
    var req = {
      "brand": brand,
      "model": model,
      "hash": hash,
    };

    var rsp = await _apiRequest(method, bizUrl, body: jsonEncode(req));
    var devInfo = unmarshalJson<Map<String, dynamic>>(rsp);
    return devInfo['device_id'];
  }
}

typedef factoryConstructor<T> = T Function(Map<String, dynamic>);
